# dgr.py - A DGR.
# Copyright 2009 Rob Myers <rob@robmyers.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

################################################################################
# Imports
################################################################################

import logging
import random
import re
import datetime
import post_data

import feedparser
import sys
import traceback
import twitter
import urllib2
import yaml

import tweet

from google.appengine.api import urlfetch 
from googlemaps import GoogleMaps
from google.appengine.ext import db

#Constant declaration
SMP_NEW_DESTINATION = 100  #for saveMapPoint indicates a new destination was saved
SMP_NO_NEW_DESTINATION = 500 #for saveMapPoint indicates destination same as current location was saved

################################################################################
# dgr
################################################################################

class SavedMapPoint(db.Model):
  '''This is the data store class that is used to save map points.
  
  This class is used to save a point on the map.  It saves information so that 
  the point can be completely recreated on a map with the information here, 
  including the information about the point such as the street address, city, 
  phone number, etc.  This point is associated with a user, so that each user 
  can have their own saved points.  

  Attributes:
    user: A UserProperty to associate the record with a user.  Optional --
      There are two times to use SavedMapPoints.  One is when the user searched   
      for a point and decided to click "save" on it.  The other is when the 
      application saves a LastPoint (the last point the user was looking at).  
      In this case, we create a SavedMapPoint that the LastPoint has a 
      reference to.  In case one (user clicks save) we assign a user, in case 2 
      (SavedMapPoint is from LastPoint) we don't assign a user.  That way when 
      we pull a user's "saved points" we don't accidentally grab their "last 
      point" because the user didn't choose to save it so we shouldn't display 
      it as their "saved point".
    lat: String of the latitude of the point.
    lng: String of the longitude of the point.
    title: String of the title of the point.
    url: String of the url of the point.
    street_address: String of the street address of the point.
    region: String of the region of the point.
    city: String of the city of the point.
    date: DateTime the point was saved at.  Optional, defaults to Now.
    zoom_level: String of the zoom level of the point.
    phone: String of the phone number of the point.
  '''
  user = db.StringProperty()
  lat = db.StringProperty()
  lng = db.StringProperty()
  title = db.StringProperty()
  url = db.StringProperty()
  street_address = db.StringProperty()
  region = db.StringProperty()
  city = db.StringProperty()
  date = db.DateTimeProperty(auto_now_add=True)
  zoom_level = db.StringProperty()
  phone = db.StringProperty()
  address_lines = db.StringProperty()
  timeToGetHere = db.IntegerProperty()

class SavedDirection(db.Model):
  '''This is the data store class that is used to save map points.
  
  This class is used to save a point on the map.  It saves information so that 
  the point can be completely recreated on a map with the information here, 
  including the information about the point such as the street address, city, 
  phone number, etc.  This point is associated with a user, so that each user 
  can have their own saved points.  

  Attributes:
    user: A UserProperty to associate the record with a user.  Optional --
      There are two times to use SavedMapPoints.  One is when the user searched   
      for a point and decided to click "save" on it.  The other is when the 
      application saves a LastPoint (the last point the user was looking at).  
      In this case, we create a SavedMapPoint that the LastPoint has a 
      reference to.  In case one (user clicks save) we assign a user, in case 2 
      (SavedMapPoint is from LastPoint) we don't assign a user.  That way when 
      we pull a user's "saved points" we don't accidentally grab their "last 
      point" because the user didn't choose to save it so we shouldn't display 
      it as their "saved point".
    lat: String of the latitude of the point.
    lng: String of the longitude of the point.
    title: String of the title of the point.
    url: String of the url of the point.
    street_address: String of the street address of the point.
    region: String of the region of the point.
    city: String of the city of the point.
    date: DateTime the point was saved at.  Optional, defaults to Now.
    zoom_level: String of the zoom level of the point.
    phone: String of the phone number of the point.
  '''
  user = db.StringProperty()
  lat = db.StringProperty()
  lng = db.StringProperty()
  title = db.StringProperty()
  url = db.StringProperty()
  street_address = db.StringProperty()
  region = db.StringProperty()
  city = db.StringProperty()
  date = db.DateTimeProperty(auto_now_add=True)
  zoom_level = db.StringProperty()
  phone = db.StringProperty()
  address_lines = db.StringProperty()
  
class tUser(db.Model):
  '''This is the data store class that is used to save users.
  
  This class is used to save a user - which is a DGR.  It saves information so that 
  the complete list of users can be easily accessed. This model will also contain twitter account details 
  Now extended to contain the last known location - for use with Layars - will also contain Layar specific info for creating POI
  
  Attributes:
    user: username of the twitteraccount
    password: String of the password of the twitter account.
    date: timestamp
    coordinates
  '''
  user = db.StringProperty()
  type = db.StringProperty()
  date = db.DateTimeProperty(auto_now_add=True)
  coordinates = db.StringProperty()
  lat = db.FloatProperty()
  lng = db.FloatProperty()
  image_url = db.StringProperty()
  line2 = db.StringProperty()
  line3 = db.StringProperty() 
  line4 = db.StringProperty()
  LayarType = db.IntegerProperty()
  dimension = db.IntegerProperty()
  
class dgrWalking():

    #def __init__ (self, config_path, state, log, server_config, live=True):
    def __init__ (self, config_path):
        '''Set up the dgr-walking configuration from YAML.'''

        self.config_path = config_path
        self.twitter_api = False
        self.hasRoutine = False
        self.currLocation = ''
        self.configure ()

        
    def configure (self):
        '''Deserialize the configuration.'''
        # Load configuration
        yaml_stream = file (self.config_path, 'r')
        config = yaml.safe_load (yaml_stream)
        '''self.twitter_username = config["twitter_username"]
        self.twitter_password = config["twitter_password"]
        self.wff_search = config["wff_search"]
        self.emotion_keywords = config["emotion_keywords"]
        self.time_keywords = config["time_keywords"]
        if (("weekend_time_keywords" in config) and
            (str (config["weekend_time_keywords"]).strip() != "")):
            self.weekend_time_keywords = config["weekend_time_keywords"]
        else:
            self.weekend_time_keywords = config["time_keywords"]
        self.character_keywords = config["character_keywords"]
        '''
        #get gmaps api key - this should come from yaml
        self.gmapsKey = config["gmapKey"]
        logging.debug ('gmapKey = ' + self.gmapsKey)
        if 'routine' in config:
            self.hasRoutine = True
            self.routineKeywords = config["routine"]
        else:
            self.hasRoutine = False
            
        self.home = config["home"]
        self.work = config["work"]
        #set destination keywords
        self.twitteruser = config["twitter_username"]

        self.destination_location_keywords = config["destination_location_keywords"]
        self.time_location_keywords = config["time_location_keywords"]
        self.feeling_location_keywords = config["feeling_location_keywords"]
        self.ensure_loctime_keywords_keys_are_ints ()
        #terms to use when posting locations
        self.locationPostTerms = config["location_post_terms"]

    def ensure_loctime_keywords_keys_are_ints (self):
        '''Make sure the time keyword keys are ints, not strings'''
        for key in self.time_location_keywords.keys ():
            value = self.time_location_keywords[key]
            del self.time_location_keywords[key]
            self.time_location_keywords[int (key)] = value

    def ensure_time_keywords_keys_are_ints (self):
        '''Make sure the time keyword keys are ints, not strings'''
        for key in self.time_keywords.keys ():
            value = self.time_keywords[key]
            del self.time_keywords[key]
            self.time_keywords[int (key)] = value

    def walk (self, searchTerms, twitter_api=None):
        '''Get a destination based on current location'''
        logging.debug ("getting location for " + '' .join(searchTerms) + ".")
        located = False
        destinations = []
        gmaps = GoogleMaps(self.gmapsKey)
               
        #destination = self.getDestination(searchTerms)
        #where we are
        currentLocation = self.getCurrentLocation() #lat,lng of last stored destination (we are assuming that the bot travelled to this location since last run)
        #where we are going
        destinationType = self.getSearchTerm(searchTerms) #gets a single search term based on supplied searchTerm dict = WFF feed and character settings - much room for improvement here
        #logging.debug ('destinationType is ' + str(destinationType))
        #if search term is none then we have no destination so go home or work
        if destinationType is None:
            #then we shouldn't go anywhere - and we should be at the correct destination
            logging.debug("destinationType is None")
            if (self.currLocation == self.home):
                destinations = self.home #self.currLocation
                self.currLocation = self.home
                logging.debug("destination = home")
            else:
                destinations = self.work #self.currLocation
                self.currLocation = self.work
                logging.debug("destination = work")
                
            location = gmaps.local_search(self.currLocation) #, sll=str(lat) + ',' + str(lng))localSearch = gmaps.local_search(currentLocation) #, sll=str(lat) + ',' + str(lng))
            lat, lng = gmaps.address_to_latlng(self.currLocation)
            
        else:
            #self.connect_to_twitter()
    		#set twitter user location
            logging.debug ("gmaps.address_to_latlng - " + str(self.currLocation))
            try: 
                lat, lng = gmaps.address_to_latlng(self.currLocation)
                #get dictionary of results - currently only returning a maximum of 8 results - something to do with the API and JSON
                location = gmaps.local_search(self.currLocation) #, sll=str(lat) + ',' + str(lng))localSearch = gmaps.local_search(currentLocation) #, sll=str(lat) + ',' + str(lng))
                logging.debug ('search = ' + self.currLocation)
                
            except Exception, err:
                #for some reason we couldn't convert the address to lat/lng, this seems to happen from time to time, so we either go back home, or go back to work depending on the time of day
                #need to figure out how to get usre local time, not server time to make this more appropriate - I think it's using NY time.
                currentLocation = self.isHomeOrWork()
                location = gmaps.local_search(self.currLocation) #, sll=str(lat) + ',' + str(lng))localSearch = gmaps.local_search(currentLocation) #, sll=str(lat) + ',' + str(lng))
                lat, lng = gmaps.address_to_latlng(self.currLocation)
                logging.debug("error is " + str(err))
                
                
                pass #ignore for now
                #sys.stderr.write('ERROR: %s\n' % str(err))
                #return 1            
            logging.info('user location: ' + str(lat) + ',' + str(lng) + str(self.currLocation)) 
                
             
            #get dictionary of results - currently only returning a maximum of 8 results - something to do with the API and JSON
            localSearch = gmaps.local_search(str(destinationType) + ' loc: ' + self.currLocation) #, sll=str(lat) + ',' + str(lng))
            logging.debug ('search = ' + str(destinationType) + ' loc: ' + self.currLocation)
                
            #shuffle the destinations so we don't always go to the same place
            destinations = localSearch['responseData']['results']
            random.shuffle (destinations)
       

        logging.debug ("update profile location")
        #twitter_api object passed from dgr when instantiating class
        try:
            #twitter_api.UpdateProfileLocation (str(lat) + ',' + str(lng))
            pass
        except Exception, err:
            logging.info ('problem with twitter' + str(err))
            pass

        #this bit is to update the current location of the bot, we will use this information to create the LAYAR
        
        #need to get last stored destination
        logging.debug(" save tuser now")
        if currentLocation is not None:
            logging.debug ('tuser = ' + str(currentLocation.title))
            tuser = self.getTUser(self.twitteruser)
            tuser.coordinates = (str(currentLocation.lat) + ',' + str(currentLocation.lng))
            tuser.lat = float(currentLocation.lat)
            tuser.lng = float(currentLocation.lng)
            tuser.image_url = None
            tuser.line2 = currentLocation.title
            tuser.line3 = currentLocation.address_lines
            tuser.line4 = currentLocation.city
            LayarType = 0 #this is hardcoded for now - but we will change when we have 3D data
            dimension = None #uses default 1d in LAYAR
            
            tuser.put();
        else:
            logging.debug ('no destination')
            
        logging.debug ("walked.")
        return destinations

    def getCurrentLocation(self):
		#set location based on time
        '''Get the last destination from the datastore
            Returns 
        '''
        
        #here we need to find out whether we are working on a routine:
        if self.hasRoutine:
            #know where we should be for the routine
            self.currLocation = self.isHomeOrWork()
            locations = db.GqlQuery('SELECT * FROM SavedMapPoint where user = :1 order by date desc LIMIT 1', self.twitteruser)
            try:
                location = locations.get()
            except Exception, err:
                locations = db.GqlQuery('SELECT * FROM SavedMapPoint order by date desc LIMIT 1')
                if (len(locations) == 0):
                    location = None
                
                #for some reason we couldn't convert the address to lat/lng, this seems to happen from time to time, 
                #so we either go back home, or go back to work depending on the time of day - this is handled in the caller
                logging.debug("get address error is " + str(err))

            if location is None:
                #then we should decide to go home or to work
                logging.debug("no saved locations for " + self.twitteruser)
                self.currLocation = self.isHomeOrWork()
            else:
                logging.info ('next location = ' + location.address_lines.replace('London', ' London'))
                #self.currLocation = location.street_address + ' ' + location.city + ' ' + location.region
                
                #THIS SHOULD BE SET TO THE SAVEDMAPPOINT OBJECT
                self.currLocation = location.address_lines.replace('London', ' London')
        else:
    		#search data store 
            locations = db.GqlQuery('SELECT * FROM SavedMapPoint where user = :1 order by date desc LIMIT 1', self.twitteruser)
            location = locations.get()
            logging.debug("did we get a location")
            if location is None:
                #then we should decide to go home or to work
                logging.debug("no saved locations for " + self.twitteruser)
                self.currLocation = self.isHomeOrWork()
            else:
                logging.info ('next location = ' + location.address_lines.replace('London', ' London'))
                #self.currLocation = location.street_address + ' ' + location.city + ' ' + location.region
                self.currLocation = location.address_lines.replace('London', ' London')
        
        return location
        
    def getAddress(self, location):
        ''' Function to return address from localsearch '''
        gmaps = GoogleMaps(self.gmapsKey)
        try: 
            lsResults = gmaps.local_search(location)
            #get dictionary of results - currently only returning a maximum of 8 results - something to do with the API and JSON
            logging.debug ('get address = ' + location)
            
        except Exception, err:
            #for some reason we couldn't convert the address to lat/lng, this seems to happen from time to time, 
            #so we either go back home, or go back to work depending on the time of day - this is handled in the caller
            logging.debug("get address error is " + str(err))
        return lsResults['responseData']['results']
        
    def isHomeOrWork(self):
        logging.debug ("running is home or work")
        hour = datetime.datetime.utcnow().hour
        if (hour >= 9 and hour <=18):
            return self.work

        else:
            return self.home

    def getRoutine(self):
        hour = datetime.datetime.utcnow().hour
        routines = self.routineKeywords.keys()
        #self.currLocation = self.isHomeOrWork()
       
        keywords = None
        for time in routines:       
            if (int(hour) == int(time)):
                logging.info("routine = " + self.routineKeywords[time])
                keywords = self.routineKeywords[time]
                break

        if (keywords is None):
            logging.info (' - keywords = 0 - hour = ' + str(hour))
            return None
        else:        
            logging.info ("Time of day location keywords: " + ", " + str(keywords))
            return keywords

    def getFeelingLocation(self, emotions):
        '''Get the keyword list for all the emotions'''
        # Or just for the first match?
        keywords = []
        for emotion in emotions:
            if (emotion in self.feeling_location_keywords):
                keywords += self.feeling_location_keywords[emotion]
            logging.info (emotion)
        logging.info ("Emotions keywords: " + ", ".join (keywords))
        return keywords		
		
    def getSearchTerm(self, searchTerms):
        keywords = []
        logging.debug ('hasRoutine = ' + str(self.hasRoutine))
        if self.hasRoutine:
            keyword = self.getRoutine()
            if keyword is None:
                logging.debug('getSearchTerm keyword none')
                self.getSearchTerm = None
            else:
                logging.debug('getSearchTerm keyword not none')
                self.getSearchTerm = keyword
            
        else:           
            random.shuffle (searchTerms)
            for searchTerm in searchTerms:
                if (searchTerm in self.destination_location_keywords):
                    self.getSearchTerm = self.destination_location_keywords[searchTerm]
                    break
                #logging.info (emotion)
        #logging.info ("Destination keywords: " + ", ".join(self.getSearchTerm))
        return self.getSearchTerm
	
    def getTUser(self, twitter_username):
        '''gets a tuser model from the database for a given user'''
        try:
            users = db.GqlQuery('SELECT * FROM tUser where user = :1', twitter_username)
            user = users.get()
        except Exception, err:
            user = None
        
        return user
    
    
    def addTUser(self, twitter_username, twitter_password):
        '''Saves a user to the database.

        This method is used to create a list of dgrs being run on the dgr runner.

        Args:
        twitter_user: twitter user name.
        twitter_password: twitter account password.

        Returns:
        Nothing
        '''  
        
        #before we save the user first check if it already exists
        user = self.getTUser(twitter_username)
        logging.debug("did we find a user")
        if user is None:
            tuser = tUser()
            tuser.user = self.twitteruser
            tuser.password = twitter_password
            key = tuser.put()
            logging.debug ('saved user for ' + self.twitteruser + ' at ' + str(datetime.datetime.utcnow()))        
        
    def SaveDestination(self, lat, lng, title, url, street_address, region, city, zoom_level, phone, addresslines):
        '''Saves last map view in datastore so last view can be restored.

        This method is used when we want to save the last single point that a user 
        was looking at (we use a different method if they were looking at multiple 
        points).  This method will update the LastPosition for the user, and if the 
        LastPosition points to a SavedMapPoint then this method will update/delete it 
        as necessary.

        Args:
        lat: Latitude of point to save.
        lng: Longitude of point to save.
        title: Title of point to save.
        url: Url of point to save.
        street_address: The street address of point to save.
        region: The region of the point to save.
        city: The city of the point to save.
        zoom_level: The zoom level that was used when saving.
        phone: The phone number of the point to save.

        Returns:
        Constants indicating a new destination was found, or if existing destination same as current location
        SMP_NEW_DESTINATION = 100  #for saveDestination indicates a new destination was saved
        SMP_NO_NEW_DESTINATION #for saveDestination indicates destination same as current location was saved
        '''

        #before we save the date we should figure out the time to get to our destination
        start = self.getCurrentLocation()
        end = addresslines
             
        if (start != None and str(start.address_lines) != end):
            logging.debug('saving map point - getDirections - ' + str(start.address_lines) + '--' + end)
            try:
                dirs = self.getDirections(start, end)
            except Exception, err:
                dirs = None
                pass
        else:
            logging.debug('saving map point - getDirections - no current location')
            dirs = None
        
        saved_map_point = SavedMapPoint()
        saved_map_point.user = self.twitteruser
        saved_map_point.lat = lat
        saved_map_point.lng = lng
        saved_map_point.title = title
        saved_map_point.url = url
        saved_map_point.street_address = street_address
        saved_map_point.region = region
        saved_map_point.city = city
        saved_map_point.zoom_level = zoom_level
        saved_map_point.phone = phone
        saved_map_point.address_lines = addresslines
        if dirs is not None:
            saved_map_point.timeToGetHere = dirs['Directions']['Duration']['seconds']
        key = saved_map_point.put()
        logging.debug ('saved map point for ' + self.twitteruser + ' at ' + str(datetime.datetime.utcnow()))
        
        if (str(start.address_lines) == end):
            logging.debug('saving map point - current location is same as destination')
            return SMP_NO_NEW_DESTINATION
        else:
            return SMP_NEW_DESTINATION
        
        
        #last_point.saved_map_key = str(key)
        #last_point.user = users.get_current_user()
        #last_point.put() 
        
    def getDirections(self, start, end):
        ''' this will call the googlemaps.py module to retrieve a set of directions
            we will then store the resulting route in a route table, and associate the directions with the map points
        '''
        #get he googlemaps api
        gmaps = GoogleMaps(self.gmapsKey) 
        
        #now get directions between to and frm - mostly interested in the time to travel at the moment.
        myDirections = gmaps.directions(start, end)
        logging.debug ('got directions will take ' + str(myDirections['Directions']['Duration']['seconds']) + ' seconds to get there')
            
        #shuffle the destinations so we don't always go to the same place
        #myRoute = directions['Directions']['Routes'][0]
        
        return myDirections

    def postLocation(self):
        currLocation = self.getCurrentLocation()
        terms = self.locationPostTerms
        random.shuffle(terms)
        
        message_body = terms[0] 
        #locURL = currLocation.url
        logging.debug (" this is the curr location " + str(currLocation.address_lines))
        if (currLocation.url.find("/place") != -1):
            #mapURLs = currLocation.url.split("/place")
            #if mapURLs
            locationURL = currLocation.url.split("/place")[0] + currLocation.url.split("/place")[1]
        else:
            locationURL = currLocation.url
            
        return post_data.PostData(description=None, 
                                  title=terms[0] + " " + currLocation.title,
                                  user_name=currLocation.user,
                                  user_url=None,
                                  source_url=None, 
                                  image_url=None,                                  
                                  embed_html=locationURL
                                  )
    
    
class dgrDirections():
    '''this is the class that will work out the directions, get the times (eta) for each step and then log them in the database 
      we can use these directions - specifically time to set up TASKS in the task queue
      am thinking that this combined with street view, could give an approximate real time update view of walking down a street.
      '''
    def __init__ (self, config_path):
        '''Set up the dgr-directions configuration from YAML.'''

        self.config_path = config_path
        self.twitter_api = False
        self.hasRoutine = False
        self.currLocation = ''
        self.configure ()

        
    def configure (self):
        '''Deserialize the configuration.'''
        # Load configuration
        yaml_stream = file (self.config_path, 'r')
        config = yaml.safe_load (yaml_stream)
        
        #get gmaps api key - this should come from yaml
        self.gmapsKey = config["gmapKey"]
        logging.debug ('gmapKey = ' + self.gmapsKey)
        if 'routine' in config:
            self.hasRoutine = True
            self.routineKeywords = config["routine"]
        else:
            self.hasRoutine = False
            
        self.home = config["home"]
        self.work = config["work"]
        #set destination keywords
        self.twitteruser = config["twitter_username"]
        self.destination_location_keywords = config["destination_location_keywords"]
        self.time_location_keywords = config["time_location_keywords"]
        self.feeling_location_keywords = config["feeling_location_keywords"]
        self.ensure_loctime_keywords_keys_are_ints ()
        #self.ensure_time_keywords_keys_are_ints ()

    def getDirections(self, start, end):
        ''' this will call the googlemaps.py module to retrieve a set of directions
            we will then store the resulting route in a route table, and associate the directions with the map points
        '''
        #get he googlemaps api
        gmaps = GoogleMaps(self.gmapsKey) 
        
        #now get directions between to and frm - mostly interested in the time to travel at the moment.
        myDirections = gmaps.directions(start, end)
        logging.debug ('got directions will take ' + myDirections['Directions']['Duration']['html'] + ' to get there')
            
        #shuffle the destinations so we don't always go to the same place
        myRoute = directions['responseData']['results']
        random.shuffle (destinations)
        
